# dual_timeframe_xgboost.py
# -*- coding: utf-8 -*-
import warnings
warnings.filterwarnings("ignore")

from dataclasses import dataclass
from typing import Tuple, Optional, Dict

import numpy as np
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
from xgboost import XGBClassifier
import matplotlib.pyplot as plt
import matplotlib.dates as mdates


# ===============================
# 1) 常用技术指标（不依赖第三方TA库）
# ===============================
def ema(s: pd.Series, span: int) -> pd.Series:
    return s.ewm(span=span, adjust=False).mean()

def rsi(close: pd.Series, period: int = 14) -> pd.Series:
    delta = close.diff()
    up = delta.clip(lower=0.0)
    down = -delta.clip(upper=0.0)
    roll_up = ema(up, period)
    roll_down = ema(down, period)
    rs = roll_up / (roll_down + 1e-12)
    return 100 - (100 / (1 + rs))

def macd(close: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Tuple[pd.Series, pd.Series, pd.Series]:
    macd_line = ema(close, fast) - ema(close, slow)
    signal_line = ema(macd_line, signal)
    hist = macd_line - signal_line
    return macd_line, signal_line, hist

def atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
    high, low, close = df["high"], df["low"], df["close"]
    prev_close = close.shift(1)
    tr = pd.concat([
        (high - low),
        (high - prev_close).abs(),
        (low - prev_close).abs()
    ], axis=1).max(axis=1)
    return ema(tr, period)

def bollinger(close: pd.Series, window: int = 20, nstd: float = 2.0) -> Tuple[pd.Series, pd.Series, pd.Series]:
    ma = close.rolling(window).mean()
    sd = close.rolling(window).std(ddof=0)
    upper = ma + nstd * sd
    lower = ma - nstd * sd
    return ma, upper, lower


# ===============================
# 2) 特征工程
# ===============================
def add_common_features(df: pd.DataFrame, prefix: str = "") -> pd.DataFrame:
    """对任意K线级别添加通用特征，并统一做1期shift防止未来函数。"""
    out = df.copy()

    # 收益与波动
    out[f"{prefix}ret_1"]   = out["close"].pct_change(1)
    out[f"{prefix}ret_5"]   = out["close"].pct_change(5)
    out[f"{prefix}ret_20"]  = out["close"].pct_change(20)
    out[f"{prefix}vol_20"]  = out[f"{prefix}ret_1"].rolling(20).std(ddof=0)

    # 移动均线与斜率
    for w in [5, 10, 20, 50, 100]:
        out[f"{prefix}ma_{w}"] = out["close"].rolling(w).mean()
        out[f"{prefix}ema_{w}"] = ema(out["close"], w)
        out[f"{prefix}ma_slope_{w}"] = out[f"{prefix}ma_{w}"].diff()

    # RSI / MACD / ATR / 布林
    out[f"{prefix}rsi_14"] = rsi(out["close"], 14)
    macd_line, macd_sig, macd_hist = macd(out["close"])
    out[f"{prefix}macd"] = macd_line
    out[f"{prefix}macd_sig"] = macd_sig
    out[f"{prefix}macd_hist"] = macd_hist
    out[f"{prefix}atr_14"] = atr(out, 14)

    bb_ma, bb_up, bb_low = bollinger(out["close"], 20, 2.0)
    out[f"{prefix}bb_ma_20"] = bb_ma
    out[f"{prefix}bb_up_20"] = bb_up
    out[f"{prefix}bb_low_20"] = bb_low
    out[f"{prefix}bb_pos_20"] = (out["close"] - bb_low) / (bb_up - bb_low + 1e-12)

    # 量能
    out[f"{prefix}vol_ma_20"] = out["volume"].rolling(20).mean()
    out[f"{prefix}vol_ratio"] = out["volume"] / (out[f"{prefix}vol_ma_20"] + 1e-12)

    # 价差与当日波幅（对短周期更有用）
    out[f"{prefix}hl_range"] = (out["high"] - out["low"]) / out["close"].shift(1)

    # ——关键：所有特征整体后移一根，避免用到当前K线的收盘后信息——
    feature_cols = [c for c in out.columns if c not in ["open", "high", "low", "close", "volume"]]
    out[feature_cols] = out[feature_cols].shift(1)

    return out


# ===============================
# 3) 数据集构造（从15分钟重采样得到日线）
# ===============================
def resample_daily(df_15m: pd.DataFrame) -> pd.DataFrame:
    d = df_15m.resample("1D").agg({
        "open": "first",
        "high": "max",
        "low":  "min",
        "close":"last",
        "volume":"sum"
    })
    d = d.dropna(subset=["open", "high", "low", "close"])
    return d

def build_labeled_dataset(
    df: pd.DataFrame,
    horizon: int,
    eps: float,
    freq_tag: str
) -> Tuple[pd.DataFrame, pd.Series]:
    """
    df: 含指标的DataFrame（索引为时间）
    horizon: 前瞻周期（天或bar）
    eps: 标签阈值（>eps: 上涨；<-eps: 下跌；其余：震荡）
    freq_tag: 仅用于命名，'D' 或 '15m'
    """
    out = df.copy()
    fwd_ret = out["close"].shift(-horizon) / out["close"] - 1.0
    out[f"fwd_ret_{freq_tag}_{horizon}"] = fwd_ret

    # 多分类标签：-1 / 0 / +1 ——> 映射到 0/1/2
    y_raw = pd.Series(np.where(
        fwd_ret > eps, 1,
        np.where(fwd_ret < -eps, -1, 0)
    ), index=out.index)

    # 丢弃未来不可见的末尾行
    out = out.iloc[:-horizon]
    y_raw = y_raw.iloc[:-horizon]

    # 取出特征列
    drop_cols = ["open","high","low","close","volume", f"fwd_ret_{freq_tag}_{horizon}"]
    X = out.drop(columns=[c for c in drop_cols if c in out.columns])
    y = (y_raw + 1)  # -1/0/1 -> 0/1/2

    # 去掉含NaN的首段
    valid = X.dropna().index
    X = X.loc[valid]
    y = y.loc[valid]
    return X, y


# ===============================
# 4) 训练 / 验证工具
# ===============================
@dataclass
class ModelBundle:
    model: XGBClassifier
    feature_cols: list
    label_horizon: int
    eps: float
    freq_tag: str  # 'D' or '15m'

def split_train_val_by_time(X: pd.DataFrame, y: pd.Series, val_ratio: float = 0.2):
    n = len(X)
    cut = int(n * (1 - val_ratio))
    X_train, X_val = X.iloc[:cut], X.iloc[cut:]
    y_train, y_val = y.iloc[:cut], y.iloc[cut:]
    return X_train, X_val, y_train, y_val

def train_xgb_multi_class(X_train, y_train, X_val, y_val, num_class: int = 3) -> XGBClassifier:
    model = XGBClassifier(
        objective="multi:softprob",
        num_class=num_class,
        n_estimators=1200,
        learning_rate=0.03,
        max_depth=6,
        subsample=0.9,
        colsample_bytree=0.9,
        reg_lambda=2.0,
        gamma=0.0,
        tree_method="hist",
        random_state=42
    )
    model.fit(
        X_train, y_train,
        eval_set=[(X_val, y_val)],
        verbose=False
    )
    return model

def walk_forward_oof_proba(
    X: pd.DataFrame, y: pd.Series, n_splits: int = 5
) -> pd.DataFrame:
    """
    Walk-Forward 产生每个时间点的“类外（OOF）”概率，避免信息泄露。
    返回列：['p_down','p_flat','p_up']，与y的0/1/2对应。
    """
    proba = pd.DataFrame(index=X.index, columns=["p_down","p_flat","p_up"], dtype=float)
    tscv = TimeSeriesSplit(n_splits=n_splits)
    for train_idx, test_idx in tscv.split(X):
        X_tr, X_te = X.iloc[train_idx], X.iloc[test_idx]
        y_tr, y_te = y.iloc[train_idx], y.iloc[test_idx]
        mdl = train_xgb_multi_class(X_tr, y_tr, X_te, y_te, num_class=3)
        p = pd.DataFrame(mdl.predict_proba(X_te), index=X_te.index, columns=["p_down","p_flat","p_up"])
        proba.loc[X_te.index, :] = p
    return proba.astype(float)


# ===============================
# 5) 顶层流程：训练日线模型 & 15分钟模型
# ===============================
def train_daily_model_from_15m(
    df_15m: pd.DataFrame,
    daily_horizon_days: int = 3,
    daily_eps: float = 0.004,     # ~0.4%: 可按标的波动性微调
    val_ratio: float = 0.2
) -> Tuple[ModelBundle, pd.DataFrame]:
    """
    从15分钟数据重采样成日线，做特征、打标并训练。
    返回：日线模型bundle，以及其OOF概率（按日）
    """
    daily = resample_daily(df_15m)
    daily_feat = add_common_features(daily, prefix="D_")
    Xd, yd = build_labeled_dataset(daily_feat, daily_horizon_days, daily_eps, "D")

    Xd_tr, Xd_val, yd_tr, yd_val = split_train_val_by_time(Xd, yd, val_ratio)
    daily_model = train_xgb_multi_class(Xd_tr, yd_tr, Xd_val, yd_val, num_class=3)

    print("\n[日线模型 验证集表现]")
    print(classification_report(yd_val, daily_model.predict(Xd_val), digits=4))
    print(confusion_matrix(yd_val, daily_model.predict(Xd_val)))

    # Walk-Forward OOF 概率（训练全集上执行，给15分钟模型做“日线信号”特征）
    daily_oof = walk_forward_oof_proba(Xd, yd, n_splits=5)
    # 为了在“当天盘中”使用，使用“前一交易日”的OOF概率作为当日偏向
    daily_oof_for_intraday = daily_oof.shift(1)

    bundle = ModelBundle(
        model=daily_model,
        feature_cols=list(Xd.columns),
        label_horizon=daily_horizon_days,
        eps=daily_eps,
        freq_tag="D"
    )
    # 返回两个：bundle + 给盘中使用的“前一日”OOF
    return bundle, daily_oof_for_intraday

def train_intraday_model(
    df_15m: pd.DataFrame,
    daily_bias_oof: Optional[pd.DataFrame] = None,
    intraday_horizon_bars: int = 4,
    intraday_eps: float = 0.0005,  # ~5bp
    val_ratio: float = 0.2
) -> Tuple[ModelBundle, Dict[str, pd.DataFrame]]:
    """
    训练15分钟择时模型。可选地引入“上一交易日的日线OOF概率”作为额外特征。
    返回：模型bundle + 训练/验证集的预测概率（便于后续融合/回测）
    """
    m15 = df_15m.copy()
    m15_feat = add_common_features(m15, prefix="M15_")

    # 若提供了日线OOF概率，把它映射到每个15m bar（按自然日对齐、前一日概率）
    if daily_bias_oof is not None:
        daily_bias_oof = daily_bias_oof.copy()
        daily_bias_oof.columns = [f"daily_{c}_prev" for c in daily_bias_oof.columns]
        # 先把15m数据的日期对齐到"自然日"
        key_day = m15_feat.index.floor("D")
        # 创建临时DataFrame用于join
        temp_df = pd.DataFrame(index=m15_feat.index)
        temp_df['date_key'] = key_day
        # 将日线概率扩展到15m频率（前向填充到当天所有bar）
        for col in daily_bias_oof.columns:
            temp_df[col] = temp_df['date_key'].map(daily_bias_oof[col])
        # 前向填充缺失值
        temp_df[daily_bias_oof.columns] = temp_df[daily_bias_oof.columns].fillna(method='ffill')
        # 合并到特征数据中，排除临时的日期列
        m15_feat = pd.concat([m15_feat, temp_df[daily_bias_oof.columns]], axis=1)

    Xm, ym = build_labeled_dataset(m15_feat, intraday_horizon_bars, intraday_eps, "15m")

    Xm_tr, Xm_val, ym_tr, ym_val = split_train_val_by_time(Xm, ym, val_ratio)
    m15_model = train_xgb_multi_class(Xm_tr, ym_tr, Xm_val, ym_val, num_class=3)

    print("\n[15分钟模型 验证集表现]")
    print(classification_report(ym_val, m15_model.predict(Xm_val), digits=4))
    print(confusion_matrix(ym_val, m15_model.predict(Xm_val)))

    # 训练/验证集的预测概率（后续融合、回测会用到）
    proba_tr = pd.DataFrame(m15_model.predict_proba(Xm_tr), index=Xm_tr.index, columns=["p_down","p_flat","p_up"])
    proba_val = pd.DataFrame(m15_model.predict_proba(Xm_val), index=Xm_val.index, columns=["p_down","p_flat","p_up"])
    return (
        ModelBundle(
            model=m15_model,
            feature_cols=list(Xm.columns),
            label_horizon=intraday_horizon_bars,
            eps=intraday_eps,
            freq_tag="15m"
        ),
        {"train_proba": proba_tr, "val_proba": proba_val}
    )


# ===============================
# 6) 融合策略
# ===============================
def gating_signals(
    p15: pd.DataFrame,
    daily_prev: pd.DataFrame,
    thr_daily_up: float = 0.60,
    thr_daily_down: float = 0.60,
    thr_15_up: float = 0.55,
    thr_15_down: float = 0.55
) -> pd.Series:
    """
    门控融合：先看日线偏向，再决定是否启用15m信号。
    返回信号：+1（做多）/ 0（观望）/ -1（做空）
    """
    # 对齐索引（以15m索引为准）
    daily_prev = daily_prev.reindex(p15.index, method="ffill")

    p_up15 = p15["p_up"]
    p_dn15 = p15["p_down"]
    d_up = daily_prev["daily_p_up_prev"]
    d_dn = daily_prev["daily_p_down_prev"]

    sig = pd.Series(0, index=p15.index, dtype=int)
    long_mask = (d_up >= thr_daily_up) & (p_up15 >= thr_15_up)
    short_mask = (d_dn >= thr_daily_down) & (p_dn15 >= thr_15_down)
    sig[long_mask] = 1
    sig[short_mask] = -1
    return sig

def weighted_blend_scores(
    p15: pd.DataFrame,
    daily_prev: pd.DataFrame,
    alpha: float = 0.5
) -> pd.DataFrame:
    """
    加权融合：fused = alpha * daily + (1-alpha) * intraday
    仅融合多空两类的概率，震荡作为备选。
    """
    daily_prev = daily_prev.reindex(p15.index, method="ffill")
    res = pd.DataFrame(index=p15.index)
    res["fused_up"] = alpha * daily_prev["daily_p_up_prev"] + (1 - alpha) * p15["p_up"]
    res["fused_down"] = alpha * daily_prev["daily_p_down_prev"] + (1 - alpha) * p15["p_down"]
    res["fused_class"] = np.where(
        (res["fused_up"] - res["fused_down"]) > 0.05, 2,
        np.where((res["fused_down"] - res["fused_up"]) > 0.05, 0, 1)  # 2: up, 1: flat, 0: down
    )
    # 转换为交易信号
    res["signal"] = res["fused_class"].map({2: 1, 1: 0, 0: -1})
    return res

def stacking_meta_model(
    y_train: pd.Series,
    p15_train: pd.DataFrame,
    daily_prev_train: pd.DataFrame
) -> LogisticRegression:
    """
    简单堆叠：用逻辑回归学习"上行 vs 非上行"的概率，亦可复制一份做"下行 vs 非下行"。
    """
    daily_prev_train = daily_prev_train.reindex(p15_train.index, method="ffill")
    X_meta = pd.DataFrame({
        "p15_up": p15_train["p_up"],
        "p15_down": p15_train["p_down"],
        "d_up": daily_prev_train["daily_p_up_prev"],
        "d_down": daily_prev_train["daily_p_down_prev"]
    }, index=p15_train.index)
    
    # Handle None case - create dummy labels for demonstration
    if y_train is None:
        y_up = pd.Series(np.random.choice([0, 1], size=len(X_meta)), index=X_meta.index)
    else:
        y_up = (y_train == 2).astype(int)  # 预测"上行"的二分类作为示例
    
    lr = LogisticRegression(max_iter=1000, solver="lbfgs")
    lr.fit(X_meta, y_up)
    return lr


# ===============================
# 7) 简易持仓与绩效示例（可替换为更严谨回测框架）
# ===============================
def simple_pnl(
    df_15m: pd.DataFrame,
    signal: pd.Series,
    cost_bp: float = 1.0  # 单边成本（基点）
) -> pd.DataFrame:
    """
    用下一bar收盘到下一bar收盘的收益作为近似，适合演示。
    signal在[t]产生，作用于[t+1]的bar。
    """
    ret_next = df_15m["close"].pct_change().shift(-1)
    pnl = signal.shift(1).fillna(0) * ret_next
    cost = (signal.diff().abs().fillna(0) > 0).astype(float) * (cost_bp / 1e4)
    pnl_after_cost = pnl - cost
    curve = (1 + pnl_after_cost.fillna(0)).cumprod()
    return pd.DataFrame({"ret": pnl_after_cost, "equity": curve})

def plot_equity_curves(pnl_results: Dict[str, pd.DataFrame], df_price: pd.DataFrame = None):
    """
    绘制累计收益曲线
    pnl_results: {策略名称: pnl_df}
    df_price: 原始价格数据，用于绘制基准
    """
    plt.figure(figsize=(15, 8))
    
    # 绘制策略收益曲线
    for name, pnl_df in pnl_results.items():
        plt.plot(pnl_df.index, pnl_df["equity"], label=f'{name} (Final: {pnl_df["equity"].iloc[-1]:.4f})', linewidth=2)
    
    # 如果提供了价格数据，绘制买入持有基准
    if df_price is not None:
        # 计算买入持有收益
        buy_hold_ret = df_price["close"] / df_price["close"].iloc[0]
        # 对齐索引
        common_index = list(pnl_results.values())[0].index
        buy_hold_aligned = buy_hold_ret.reindex(common_index, method='ffill')
        plt.plot(common_index, buy_hold_aligned, label=f'Buy & Hold (Final: {buy_hold_aligned.iloc[-1]:.4f})', 
                linewidth=2, alpha=0.7, linestyle='--')
    
    plt.title('累计收益曲线对比', fontsize=16)
    plt.xlabel('时间', fontsize=12)
    plt.ylabel('累计收益', fontsize=12)
    plt.legend(fontsize=10)
    plt.grid(True, alpha=0.3)
    
    # 格式化x轴日期
    plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
    plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=3))
    plt.xticks(rotation=45)
    
    plt.tight_layout()
    
    # 保存图片而不是显示（适合无GUI环境）
    plt.savefig('model/equity_curves.png', dpi=300, bbox_inches='tight')
    print(f"\n累计收益曲线已保存至: model/equity_curves.png")
    plt.close()  # 释放内存

def calculate_performance_metrics(pnl_df: pd.DataFrame) -> Dict[str, float]:
    """
    计算策略绩效指标
    """
    returns = pnl_df["ret"].dropna()
    equity = pnl_df["equity"].dropna()
    
    # 年化收益率
    trading_days = 252 * 16  # 假设每天16个15分钟bar
    total_periods = len(returns)
    years = total_periods / trading_days
    annual_return = (equity.iloc[-1] / equity.iloc[0]) ** (1/years) - 1 if years > 0 else 0
    
    # 年化波动率
    annual_vol = returns.std() * np.sqrt(trading_days)
    
    # 夏普比率
    sharpe = annual_return / annual_vol if annual_vol > 0 else 0
    
    # 最大回撤
    cummax = equity.expanding().max()
    drawdown = (equity - cummax) / cummax
    max_drawdown = drawdown.min()
    
    # 胜率
    win_rate = (returns > 0).mean()
    
    # 盈亏比
    win_returns = returns[returns > 0]
    lose_returns = returns[returns < 0]
    profit_loss_ratio = abs(win_returns.mean() / lose_returns.mean()) if len(lose_returns) > 0 and lose_returns.mean() != 0 else 0
    
    return {
        "年化收益率": annual_return,
        "年化波动率": annual_vol, 
        "夏普比率": sharpe,
        "最大回撤": max_drawdown,
        "胜率": win_rate,
        "盈亏比": profit_loss_ratio,
        "总收益率": equity.iloc[-1] / equity.iloc[0] - 1,
        "交易次数": (pnl_df["ret"] != 0).sum()
    }


# ===============================
# 8) 示例主流程
# ===============================
def main(csv_15m_path: str):
    # 读取15分钟数据
    df = pd.read_parquet(csv_15m_path)
    df["datetime"] = pd.to_datetime(df["datetime"])
    df = df.sort_values("datetime").set_index("datetime")
    df = df[["open","high","low","close","volume"]].astype(float)

    # 1) 训练日线模型（从15m重采样）——并得到“上一交易日 OOF 概率”
    daily_bundle, daily_prev_oof = train_daily_model_from_15m(
        df, daily_horizon_days=3, daily_eps=0.004, val_ratio=0.2
    )
    # 为便于后续join，给列改成统一命名
    daily_prev_oof = daily_prev_oof.rename(columns={
        "p_up": "daily_p_up_prev",
        "p_down": "daily_p_down_prev",
        "p_flat": "daily_p_flat_prev"
    })

    # 2) 训练15分钟模型（引入“上一交易日”的日线OOF作为特征；也可不传）
    m15_bundle, proba_dict = train_intraday_model(
        df,
        daily_bias_oof=daily_prev_oof[["daily_p_down_prev","daily_p_flat_prev","daily_p_up_prev"]],
        intraday_horizon_bars=4,
        intraday_eps=0.0005,
        val_ratio=0.2
    )

    # 3) 融合示例（以验证集为例）
    p15_val = proba_dict["val_proba"]
    # 验证集对应的上一日OOF概率：按索引对齐并前向填充
    daily_prev_for_val = daily_prev_oof.reindex(p15_val.index, method="ffill")

    # 3.1 门控
    sig_gate = gating_signals(
        p15=p15_val,
        daily_prev=daily_prev_for_val,
        thr_daily_up=0.60, thr_daily_down=0.60,
        thr_15_up=0.55, thr_15_down=0.55
    )

    # 3.2 加权融合
    blend = weighted_blend_scores(
        p15=p15_val,
        daily_prev=daily_prev_for_val,
        alpha=0.5
    )
    sig_blend = blend["signal"]

    # 3.3 堆叠（示例：学习“上行 vs 非上行”）
    #   - 这里使用训练段构造元模型，然后在验证段出概率
    lr_meta = stacking_meta_model(
        y_train=None,  # 仅示意：上面函数需要训练段y与概率；在真实流程中请传 Xm_tr/ym_tr 对应的概率与标签
        p15_train=proba_dict["train_proba"],
        daily_prev_train=daily_prev_oof.reindex(proba_dict["train_proba"].index, method="ffill")
    )
    # 注意：上面是示例接口展示，实际调用请按训练集/验证集拆分来传入 y_train

    # 4) 计算多种策略的绩效并绘制对比
    val_data = df.loc[p15_val.index]
    
    # 计算不同策略的PnL
    pnl_gate = simple_pnl(val_data, sig_gate, cost_bp=1.0)
    pnl_blend = simple_pnl(val_data, sig_blend, cost_bp=1.0)
    
    # 纯15分钟信号（作为对比）
    sig_15m_only = pd.Series(0, index=p15_val.index)
    sig_15m_only[p15_val["p_up"] > 0.6] = 1
    sig_15m_only[p15_val["p_down"] > 0.6] = -1
    pnl_15m = simple_pnl(val_data, sig_15m_only, cost_bp=1.0)
    
    # 绘制累计收益曲线
    pnl_results = {
        "门控融合": pnl_gate,
        "加权融合": pnl_blend,
        "15分钟单独": pnl_15m
    }
    
    plot_equity_curves(pnl_results, val_data)
    
    # 打印详细绩效指标
    print("\n" + "="*60)
    print("策略绩效对比 (验证集)")
    print("="*60)
    
    for strategy_name, pnl_df in pnl_results.items():
        print(f"\n【{strategy_name}】")
        metrics = calculate_performance_metrics(pnl_df)
        for key, value in metrics.items():
            if key in ["年化收益率", "年化波动率", "最大回撤", "总收益率"]:
                print(f"  {key}: {value:.2%}")
            elif key == "胜率":
                print(f"  {key}: {value:.2%}")
            elif key in ["夏普比率", "盈亏比"]:
                print(f"  {key}: {value:.4f}")
            else:
                print(f"  {key}: {value}")
    
    print(f"\n[验证集时间范围]: {p15_val.index[0]} 至 {p15_val.index[-1]}")
    print(f"[验证集数据点数]: {len(p15_val)}")
    print("="*60)

if __name__ == "__main__":
    main("./model/im99_15min_2022_2025.parquet")
